fix: исключен approver из получателей уведомлений о решении
- Approver (тот кто принял решение) не получает уведомление о своем действии - Исключены employee.id и approver_id из recipients при approve/reject - Упрощен signal create_request_notifications (убран лишний запрос в БД) - Теперь передается актуальный request_obj с установленным approver_id
This commit is contained in:
parent
aa6fa29e3a
commit
c0b659f95d
@ -4,10 +4,10 @@ from __future__ import annotations
|
|||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from api.v1.permissions import AdminOrDeptAllowed
|
from api.v1.permissions import AdminOrDeptAllowed
|
||||||
from rest_framework.permissions import BasePermission
|
|
||||||
from rest_framework.request import Request
|
|
||||||
from employees.constants import DeptPerm
|
from employees.constants import DeptPerm
|
||||||
from employees.models import Department
|
from employees.models import Department
|
||||||
|
from rest_framework.permissions import BasePermission
|
||||||
|
from rest_framework.request import Request
|
||||||
|
|
||||||
|
|
||||||
class IsRecipientOfRequest(BasePermission):
|
class IsRecipientOfRequest(BasePermission):
|
||||||
|
|||||||
@ -2,12 +2,12 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any, List, Optional # было: Any, Dict, List, Type
|
from typing import Any, List, Optional # было: Any, Dict, List, Type
|
||||||
|
|
||||||
from django.db.models import Q, QuerySet, Count
|
from django.db.models import Count, Q, QuerySet
|
||||||
from django.shortcuts import get_object_or_404 # раскомментируйте импорт
|
from django.shortcuts import get_object_or_404 # раскомментируйте импорт
|
||||||
from employees.models import EmployeeDepartment, Department # добавлен Department
|
from employees.models import Department, EmployeeDepartment # добавлен Department
|
||||||
|
from requests_app.enums import RequestStatus
|
||||||
from requests_app.models import Request as EmployeeRequest
|
from requests_app.models import Request as EmployeeRequest
|
||||||
from requests_app.models import RequestComment
|
from requests_app.models import RequestComment
|
||||||
from requests_app.enums import RequestStatus
|
|
||||||
|
|
||||||
# from django.shortcuts import get_object_or_404 # <- не используется
|
# from django.shortcuts import get_object_or_404 # <- не используется
|
||||||
from rest_framework import status, viewsets
|
from rest_framework import status, viewsets
|
||||||
@ -58,11 +58,11 @@ class RequestViewSet(viewsets.ModelViewSet):
|
|||||||
PermissionDenied: При нарушении правил доступа.
|
PermissionDenied: При нарушении правил доступа.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
queryset = EmployeeRequest.objects.select_related(
|
queryset = (
|
||||||
"employee", "approver", "department"
|
EmployeeRequest.objects.select_related("employee", "approver", "department")
|
||||||
).annotate(
|
.annotate(comments_count=Count("comments"))
|
||||||
comments_count=Count('comments')
|
.order_by("-created_at")
|
||||||
).order_by('-created_at')
|
)
|
||||||
parser_classes = (MultiPartParser, FormParser, JSONParser)
|
parser_classes = (MultiPartParser, FormParser, JSONParser)
|
||||||
# Используем глобальную пагинацию из settings (PageNumberPagination, PAGE_SIZE=20)
|
# Используем глобальную пагинацию из settings (PageNumberPagination, PAGE_SIZE=20)
|
||||||
|
|
||||||
@ -146,17 +146,13 @@ class RequestViewSet(viewsets.ModelViewSet):
|
|||||||
elif addressed_to_me:
|
elif addressed_to_me:
|
||||||
# Заявки, где я получатель
|
# Заявки, где я получатель
|
||||||
my_dept_ids = EmployeeDepartment.objects.filter(
|
my_dept_ids = EmployeeDepartment.objects.filter(
|
||||||
employee=user,
|
employee=user, is_active=True
|
||||||
is_active=True
|
).values_list("department_id", flat=True)
|
||||||
).values_list('department_id', flat=True)
|
|
||||||
|
|
||||||
scope = (
|
scope = (
|
||||||
Q(recipients=user) |
|
Q(recipients=user)
|
||||||
Q(cc_users=user) |
|
| Q(cc_users=user)
|
||||||
Q(
|
| Q(sent_to_all_department=True, departments__in=my_dept_ids)
|
||||||
sent_to_all_department=True,
|
|
||||||
departments__in=my_dept_ids
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
qs = qs.filter(scope).distinct()
|
qs = qs.filter(scope).distinct()
|
||||||
else:
|
else:
|
||||||
@ -168,15 +164,11 @@ class RequestViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
# Заявки отделов с sent_to_all_department
|
# Заявки отделов с sent_to_all_department
|
||||||
my_dept_ids = EmployeeDepartment.objects.filter(
|
my_dept_ids = EmployeeDepartment.objects.filter(
|
||||||
employee=user,
|
employee=user, is_active=True
|
||||||
is_active=True
|
).values_list("department_id", flat=True)
|
||||||
).values_list('department_id', flat=True)
|
|
||||||
|
|
||||||
if my_dept_ids:
|
if my_dept_ids:
|
||||||
scope |= Q(
|
scope |= Q(sent_to_all_department=True, departments__in=my_dept_ids)
|
||||||
sent_to_all_department=True,
|
|
||||||
departments__in=my_dept_ids
|
|
||||||
)
|
|
||||||
|
|
||||||
# Департаментные права (как было)
|
# Департаментные права (как было)
|
||||||
view_dept_ids = (
|
view_dept_ids = (
|
||||||
@ -197,14 +189,12 @@ class RequestViewSet(viewsets.ModelViewSet):
|
|||||||
.values_list("department_id", flat=True)
|
.values_list("department_id", flat=True)
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
head_dept_ids = Department.objects.filter(
|
head_dept_ids = Department.objects.filter(head_id=user.id).values_list(
|
||||||
head_id=user.id
|
"id", flat=True
|
||||||
).values_list("id", flat=True)
|
|
||||||
|
|
||||||
combined_ids = (
|
|
||||||
set(view_dept_ids) | set(proc_dept_ids) | set(head_dept_ids)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
combined_ids = set(view_dept_ids) | set(proc_dept_ids) | set(head_dept_ids)
|
||||||
|
|
||||||
if combined_ids:
|
if combined_ids:
|
||||||
# Заявки этих отделов (новое поле departments)
|
# Заявки этих отделов (новое поле departments)
|
||||||
scope |= Q(departments__in=combined_ids)
|
scope |= Q(departments__in=combined_ids)
|
||||||
@ -224,11 +214,9 @@ class RequestViewSet(viewsets.ModelViewSet):
|
|||||||
# Фильтр "только адресованные мне"
|
# Фильтр "только адресованные мне"
|
||||||
if addressed_to_me:
|
if addressed_to_me:
|
||||||
scope = (
|
scope = (
|
||||||
Q(recipients=user) | Q(cc_users=user) |
|
Q(recipients=user)
|
||||||
Q(
|
| Q(cc_users=user)
|
||||||
sent_to_all_department=True,
|
| Q(sent_to_all_department=True, departments__in=my_dept_ids)
|
||||||
departments__in=my_dept_ids
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
elif want_mine:
|
elif want_mine:
|
||||||
scope = Q(employee_id=user.id)
|
scope = Q(employee_id=user.id)
|
||||||
@ -271,7 +259,9 @@ class RequestViewSet(viewsets.ModelViewSet):
|
|||||||
}:
|
}:
|
||||||
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
|
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
|
||||||
lookup_value = self.kwargs[lookup_url_kwarg]
|
lookup_value = self.kwargs[lookup_url_kwarg]
|
||||||
base = EmployeeRequest.objects.select_related("employee", "approver", "department")
|
base = EmployeeRequest.objects.select_related(
|
||||||
|
"employee", "approver", "department"
|
||||||
|
)
|
||||||
obj = get_object_or_404(base, **{self.lookup_field: lookup_value})
|
obj = get_object_or_404(base, **{self.lookup_field: lookup_value})
|
||||||
self.check_object_permissions(self.request, obj)
|
self.check_object_permissions(self.request, obj)
|
||||||
return obj
|
return obj
|
||||||
@ -289,7 +279,9 @@ class RequestViewSet(viewsets.ModelViewSet):
|
|||||||
extra = {"employee": user} if not is_power else {}
|
extra = {"employee": user} if not is_power else {}
|
||||||
save_as = (self.request.query_params.get("save_as") or "").strip().lower()
|
save_as = (self.request.query_params.get("save_as") or "").strip().lower()
|
||||||
if save_as == "draft":
|
if save_as == "draft":
|
||||||
extra["status"] = RequestStatus.DRAFT # ✅ обычному пользователю поле в payload всё равно бы «очистили»
|
extra["status"] = (
|
||||||
|
RequestStatus.DRAFT
|
||||||
|
) # ✅ обычному пользователю поле в payload всё равно бы «очистили»
|
||||||
|
|
||||||
serializer.save(**extra)
|
serializer.save(**extra)
|
||||||
|
|
||||||
@ -398,50 +390,34 @@ class RequestViewSet(viewsets.ModelViewSet):
|
|||||||
отвечаем read-сериализатором.
|
отвечаем read-сериализатором.
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[RequestViewSet.create] User: {request.user.id}, "
|
f"[RequestViewSet.create] User: {request.user.id}, "
|
||||||
f"Content-Type: {request.content_type}"
|
f"Content-Type: {request.content_type}"
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info(f"[RequestViewSet.create] POST keys: {list(request.POST.keys())}")
|
||||||
f"[RequestViewSet.create] POST keys: {list(request.POST.keys())}"
|
logger.info(f"[RequestViewSet.create] FILES keys: {list(request.FILES.keys())}")
|
||||||
)
|
logger.info(f"[RequestViewSet.create] Data keys: {list(request.data.keys())}")
|
||||||
logger.info(
|
|
||||||
f"[RequestViewSet.create] FILES keys: {list(request.FILES.keys())}"
|
|
||||||
)
|
|
||||||
logger.info(
|
|
||||||
f"[RequestViewSet.create] Data keys: {list(request.data.keys())}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Логируем значения для отладки
|
# Логируем значения для отладки
|
||||||
for key in ['type', 'title', 'date_from', 'date_to', 'comment']:
|
for key in ["type", "title", "date_from", "date_to", "comment"]:
|
||||||
value = request.data.get(key)
|
value = request.data.get(key)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[RequestViewSet.create] {key}: {value} "
|
f"[RequestViewSet.create] {key}: {value} (type: {type(value).__name__})"
|
||||||
f"(type: {type(value).__name__})"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
serializer = self.get_serializer(data=request.data)
|
serializer = self.get_serializer(data=request.data)
|
||||||
if not serializer.is_valid():
|
if not serializer.is_valid():
|
||||||
logger.error(
|
logger.error(
|
||||||
f"[RequestViewSet.create] Validation errors: "
|
f"[RequestViewSet.create] Validation errors: {serializer.errors}"
|
||||||
f"{serializer.errors}"
|
|
||||||
)
|
|
||||||
logger.error(
|
|
||||||
f"[RequestViewSet.create] Request data: {request.data}"
|
|
||||||
)
|
)
|
||||||
|
logger.error(f"[RequestViewSet.create] Request data: {request.data}")
|
||||||
|
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
self.perform_create(serializer)
|
self.perform_create(serializer)
|
||||||
instance = serializer.instance
|
instance = serializer.instance
|
||||||
read = RequestReadSerializer(
|
read = RequestReadSerializer(instance, context=self.get_serializer_context())
|
||||||
instance,
|
|
||||||
context=self.get_serializer_context()
|
|
||||||
)
|
|
||||||
headers = self.get_success_headers(read.data)
|
headers = self.get_success_headers(read.data)
|
||||||
return Response(
|
return Response(read.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||||
read.data,
|
|
||||||
status=status.HTTP_201_CREATED,
|
|
||||||
headers=headers
|
|
||||||
)
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from django.db.models import TextChoices
|
from django.db.models import TextChoices
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -9,12 +9,12 @@ Signals для автоматической генерации уведомле
|
|||||||
- Изменение статуса заявки
|
- Изменение статуса заявки
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
from django.db.models.signals import post_save, pre_save
|
from django.db.models.signals import post_save, pre_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.contrib.auth import get_user_model
|
from notifications.services import NotificationService
|
||||||
|
|
||||||
from .models import Request, RequestComment
|
from .models import Request, RequestComment
|
||||||
from notifications.services import NotificationService
|
|
||||||
|
|
||||||
Employee = get_user_model()
|
Employee = get_user_model()
|
||||||
|
|
||||||
@ -30,22 +30,18 @@ def create_request_notifications(sender, instance, created, **kwargs):
|
|||||||
"""
|
"""
|
||||||
request_obj = instance
|
request_obj = instance
|
||||||
|
|
||||||
if created and request_obj.status != 'draft':
|
if created and request_obj.status != "draft":
|
||||||
# Новая заявка - уведомляем ответственных
|
# Новая заявка - уведомляем ответственных
|
||||||
notify_new_request(request_obj)
|
notify_new_request(request_obj)
|
||||||
elif not created:
|
elif not created:
|
||||||
# Проверяем изменение статуса
|
# Проверяем изменение статуса через сохраненный атрибут _old_status
|
||||||
# Получаем предыдущее состояние из БД
|
if hasattr(request_obj, "_old_status"):
|
||||||
try:
|
old_status = request_obj._old_status
|
||||||
old_request = Request.objects.get(pk=request_obj.pk)
|
new_status = request_obj.status
|
||||||
if hasattr(request_obj, '_old_status'):
|
|
||||||
old_status = request_obj._old_status
|
|
||||||
new_status = request_obj.status
|
|
||||||
|
|
||||||
if old_status != new_status:
|
if old_status != new_status:
|
||||||
notify_status_change(request_obj, old_status, new_status)
|
# Передаем текущий request_obj с актуальным approver_id
|
||||||
except Request.DoesNotExist:
|
notify_status_change(request_obj, old_status, new_status)
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_save, sender=Request)
|
@receiver(pre_save, sender=Request)
|
||||||
@ -100,11 +96,15 @@ def create_comment_notification(sender, instance, created, **kwargs):
|
|||||||
|
|
||||||
# Если sent_to_all_department - все сотрудники отделов
|
# Если sent_to_all_department - все сотрудники отделов
|
||||||
if request_obj.sent_to_all_department:
|
if request_obj.sent_to_all_department:
|
||||||
dept_employees = Employee.objects.filter(
|
dept_employees = (
|
||||||
departments_links__department__in=request_obj.departments.all(),
|
Employee.objects.filter(
|
||||||
departments_links__is_active=True,
|
departments_links__department__in=request_obj.departments.all(),
|
||||||
is_active=True
|
departments_links__is_active=True,
|
||||||
).exclude(id__in=[author.id, request_obj.employee.id]).distinct()
|
is_active=True,
|
||||||
|
)
|
||||||
|
.exclude(id__in=[author.id, request_obj.employee.id])
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
recipients_set.update(dept_employees)
|
recipients_set.update(dept_employees)
|
||||||
|
|
||||||
@ -112,25 +112,26 @@ def create_comment_notification(sender, instance, created, **kwargs):
|
|||||||
for recipient in recipients_set:
|
for recipient in recipients_set:
|
||||||
NotificationService.create_notification(
|
NotificationService.create_notification(
|
||||||
recipient=recipient,
|
recipient=recipient,
|
||||||
notification_type_code='request_comment',
|
notification_type_code="request_comment",
|
||||||
title='Новый комментарий к заявке',
|
title="Новый комментарий к заявке",
|
||||||
message=(
|
message=(
|
||||||
f'{author.get_full_name() or author.username} '
|
f"{author.get_full_name() or author.username} "
|
||||||
f'прокомментировал заявку: {comment.text[:100]}'
|
f"прокомментировал заявку: {comment.text[:100]}"
|
||||||
),
|
),
|
||||||
content_object=request_obj,
|
content_object=request_obj,
|
||||||
action_url=f'/requests/{request_obj.id}/',
|
action_url=f"/requests/{request_obj.id}/",
|
||||||
metadata={
|
metadata={
|
||||||
'request_id': request_obj.id,
|
"request_id": request_obj.id,
|
||||||
'request_type': request_obj.type,
|
"request_type": request_obj.type,
|
||||||
'comment_id': comment.id,
|
"comment_id": comment.id,
|
||||||
'author_id': author.id,
|
"author_id": author.id,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ===== Вспомогательные функции =====
|
# ===== Вспомогательные функции =====
|
||||||
|
|
||||||
|
|
||||||
def notify_new_request(request_obj):
|
def notify_new_request(request_obj):
|
||||||
"""
|
"""
|
||||||
Отправляет уведомление о новой заявке:
|
Отправляет уведомление о новой заявке:
|
||||||
@ -154,11 +155,15 @@ def notify_new_request(request_obj):
|
|||||||
|
|
||||||
# 3. Если sent_to_all_department - все сотрудники отделов
|
# 3. Если sent_to_all_department - все сотрудники отделов
|
||||||
if request_obj.sent_to_all_department:
|
if request_obj.sent_to_all_department:
|
||||||
dept_employees = Employee.objects.filter(
|
dept_employees = (
|
||||||
departments_links__department__in=request_obj.departments.all(),
|
Employee.objects.filter(
|
||||||
departments_links__is_active=True,
|
departments_links__department__in=request_obj.departments.all(),
|
||||||
is_active=True
|
departments_links__is_active=True,
|
||||||
).exclude(id=request_obj.employee.id).distinct()
|
is_active=True,
|
||||||
|
)
|
||||||
|
.exclude(id=request_obj.employee.id)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
recipients_set.update(dept_employees)
|
recipients_set.update(dept_employees)
|
||||||
|
|
||||||
@ -177,25 +182,26 @@ def notify_new_request(request_obj):
|
|||||||
recipients_set.add(request_obj.department.head)
|
recipients_set.add(request_obj.department.head)
|
||||||
|
|
||||||
# 6. Пользователи с правом обрабатывать заявки в этих отделах
|
# 6. Пользователи с правом обрабатывать заявки в этих отделах
|
||||||
dept_ids = list(request_obj.departments.values_list('id', flat=True))
|
dept_ids = list(request_obj.departments.values_list("id", flat=True))
|
||||||
if request_obj.department_id and request_obj.department_id not in dept_ids:
|
if request_obj.department_id and request_obj.department_id not in dept_ids:
|
||||||
dept_ids.append(request_obj.department_id)
|
dept_ids.append(request_obj.department_id)
|
||||||
|
|
||||||
if dept_ids:
|
if dept_ids:
|
||||||
dept_processors = Employee.objects.filter(
|
dept_processors = (
|
||||||
departments_links__department_id__in=dept_ids,
|
Employee.objects.filter(
|
||||||
departments_links__is_active=True,
|
departments_links__department_id__in=dept_ids,
|
||||||
departments_links__role__scoped_permissions__code='can_process_requests',
|
departments_links__is_active=True,
|
||||||
is_active=True
|
departments_links__role__scoped_permissions__code="can_process_requests",
|
||||||
).exclude(id=request_obj.employee.id).distinct()
|
is_active=True,
|
||||||
|
)
|
||||||
|
.exclude(id=request_obj.employee.id)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
recipients_set.update(dept_processors)
|
recipients_set.update(dept_processors)
|
||||||
|
|
||||||
# Определяем тип уведомления для каждого получателя
|
# Определяем тип уведомления для каждого получателя
|
||||||
author_name = (
|
author_name = request_obj.employee.get_full_name() or request_obj.employee.username
|
||||||
request_obj.employee.get_full_name() or
|
|
||||||
request_obj.employee.username
|
|
||||||
)
|
|
||||||
|
|
||||||
for recipient in recipients_set:
|
for recipient in recipients_set:
|
||||||
# Определяем роль получателя
|
# Определяем роль получателя
|
||||||
@ -205,21 +211,21 @@ def notify_new_request(request_obj):
|
|||||||
|
|
||||||
# Формируем заголовок и сообщение
|
# Формируем заголовок и сообщение
|
||||||
if is_approver:
|
if is_approver:
|
||||||
title = f'Новая заявка на согласование от {author_name}'
|
title = f"Новая заявка на согласование от {author_name}"
|
||||||
notification_type = 'request_new'
|
notification_type = "request_new"
|
||||||
elif is_primary:
|
elif is_primary:
|
||||||
title = f'Вам адресована заявка от {author_name}'
|
title = f"Вам адресована заявка от {author_name}"
|
||||||
notification_type = 'request_new'
|
notification_type = "request_new"
|
||||||
elif is_cc:
|
elif is_cc:
|
||||||
title = f'Вы в копии заявки от {author_name}'
|
title = f"Вы в копии заявки от {author_name}"
|
||||||
notification_type = 'request_new'
|
notification_type = "request_new"
|
||||||
else:
|
else:
|
||||||
title = f'Новая заявка в отделе от {author_name}'
|
title = f"Новая заявка в отделе от {author_name}"
|
||||||
notification_type = 'request_new'
|
notification_type = "request_new"
|
||||||
|
|
||||||
message = (
|
message = (
|
||||||
f'Тип: {request_obj.get_type_display()}. '
|
f"Тип: {request_obj.get_type_display()}. "
|
||||||
f'{request_obj.comment[:100] if request_obj.comment else ""}'
|
f"{request_obj.comment[:100] if request_obj.comment else ''}"
|
||||||
)
|
)
|
||||||
|
|
||||||
NotificationService.create_notification(
|
NotificationService.create_notification(
|
||||||
@ -228,15 +234,15 @@ def notify_new_request(request_obj):
|
|||||||
title=title,
|
title=title,
|
||||||
message=message,
|
message=message,
|
||||||
content_object=request_obj,
|
content_object=request_obj,
|
||||||
action_url=f'/requests/{request_obj.id}/',
|
action_url=f"/requests/{request_obj.id}/",
|
||||||
metadata={
|
metadata={
|
||||||
'request_id': request_obj.id,
|
"request_id": request_obj.id,
|
||||||
'request_type': request_obj.type,
|
"request_type": request_obj.type,
|
||||||
'employee_id': request_obj.employee.id,
|
"employee_id": request_obj.employee.id,
|
||||||
'is_primary_recipient': is_primary,
|
"is_primary_recipient": is_primary,
|
||||||
'is_cc': is_cc,
|
"is_cc": is_cc,
|
||||||
'is_approver': is_approver,
|
"is_approver": is_approver,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -247,67 +253,86 @@ def notify_status_change(request_obj, old_status, new_status):
|
|||||||
- Всех в копии (cc_users)
|
- Всех в копии (cc_users)
|
||||||
- Сотрудников отделов (если sent_to_all_department)
|
- Сотрудников отделов (если sent_to_all_department)
|
||||||
|
|
||||||
ВАЖНО: автор не получает уведомление о решении (approve/reject),
|
ВАЖНО:
|
||||||
так как он видит результат на странице request_process.html
|
- Автор (employee) не получает уведомление о решении (approve/reject)
|
||||||
|
- Approver (тот кто принял решение) не получает уведомление о своем же решении
|
||||||
"""
|
"""
|
||||||
recipients_to_notify = set()
|
recipients_to_notify = set()
|
||||||
|
|
||||||
|
# ID пользователей которых НЕ нужно уведомлять при approve/reject
|
||||||
|
exclude_ids = set()
|
||||||
|
if new_status in ("approved", "rejected"):
|
||||||
|
# Не уведомляем автора заявки (он видит результат на странице)
|
||||||
|
exclude_ids.add(request_obj.employee.id)
|
||||||
|
# Не уведомляем того кто принял решение (он сам нажал кнопку)
|
||||||
|
if request_obj.approver_id:
|
||||||
|
exclude_ids.add(request_obj.approver_id)
|
||||||
|
|
||||||
# 1. Автор - НЕ уведомляем при approved/rejected
|
# 1. Автор - НЕ уведомляем при approved/rejected
|
||||||
# (автор видит результат прямо на странице)
|
if new_status not in ("approved", "rejected"):
|
||||||
if new_status not in ('approved', 'rejected'):
|
|
||||||
recipients_to_notify.add(request_obj.employee)
|
recipients_to_notify.add(request_obj.employee)
|
||||||
|
|
||||||
# 2. Основные получатели
|
# 2. Основные получатели (исключая тех кто в exclude_ids)
|
||||||
recipients_to_notify.update(
|
recipients_to_notify.update(
|
||||||
request_obj.recipients.filter(is_active=True)
|
request_obj.recipients.filter(is_active=True).exclude(id__in=exclude_ids)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 3. Копия
|
# 3. Копия (исключая тех кто в exclude_ids)
|
||||||
recipients_to_notify.update(
|
recipients_to_notify.update(
|
||||||
request_obj.cc_users.filter(is_active=True)
|
request_obj.cc_users.filter(is_active=True).exclude(id__in=exclude_ids)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 4. Если sent_to_all_department - все сотрудники отделов
|
# 4. Если sent_to_all_department - все сотрудники отделов (исключая exclude_ids)
|
||||||
if request_obj.sent_to_all_department:
|
if request_obj.sent_to_all_department:
|
||||||
dept_employees = Employee.objects.filter(
|
# При approve/reject также исключаем approver
|
||||||
departments_links__department__in=request_obj.departments.all(),
|
all_exclude_ids = {request_obj.employee.id}
|
||||||
departments_links__is_active=True,
|
all_exclude_ids.update(exclude_ids)
|
||||||
is_active=True
|
|
||||||
).exclude(id=request_obj.employee.id).distinct()
|
dept_employees = (
|
||||||
|
Employee.objects.filter(
|
||||||
|
departments_links__department__in=request_obj.departments.all(),
|
||||||
|
departments_links__is_active=True,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
.exclude(id__in=all_exclude_ids)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
recipients_to_notify.update(dept_employees)
|
recipients_to_notify.update(dept_employees)
|
||||||
|
|
||||||
# Формируем уведомления
|
# Формируем уведомления
|
||||||
for recipient in recipients_to_notify:
|
for recipient in recipients_to_notify:
|
||||||
if new_status == 'approved':
|
if new_status == "approved":
|
||||||
notification_type = 'request_approved'
|
notification_type = "request_approved"
|
||||||
title = 'Заявка одобрена'
|
title = "Заявка одобрена"
|
||||||
approver_name = (
|
approver_name = (
|
||||||
request_obj.approver.get_full_name()
|
request_obj.approver.get_full_name()
|
||||||
if request_obj.approver else 'Руководитель'
|
if request_obj.approver
|
||||||
|
else "Руководитель"
|
||||||
)
|
)
|
||||||
message = (
|
message = (
|
||||||
f'Заявка "{request_obj.get_type_display()}" '
|
f'Заявка "{request_obj.get_type_display()}" '
|
||||||
f'одобрена пользователем {approver_name}'
|
f"одобрена пользователем {approver_name}"
|
||||||
)
|
)
|
||||||
elif new_status == 'rejected':
|
elif new_status == "rejected":
|
||||||
notification_type = 'request_rejected'
|
notification_type = "request_rejected"
|
||||||
title = 'Заявка отклонена'
|
title = "Заявка отклонена"
|
||||||
approver_name = (
|
approver_name = (
|
||||||
request_obj.approver.get_full_name()
|
request_obj.approver.get_full_name()
|
||||||
if request_obj.approver else 'Руководитель'
|
if request_obj.approver
|
||||||
|
else "Руководитель"
|
||||||
)
|
)
|
||||||
message = (
|
message = (
|
||||||
f'Заявка "{request_obj.get_type_display()}" '
|
f'Заявка "{request_obj.get_type_display()}" '
|
||||||
f'отклонена пользователем {approver_name}'
|
f"отклонена пользователем {approver_name}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Общее уведомление об изменении статуса
|
# Общее уведомление об изменении статуса
|
||||||
notification_type = 'request_status_changed'
|
notification_type = "request_status_changed"
|
||||||
title = 'Статус заявки изменен'
|
title = "Статус заявки изменен"
|
||||||
message = (
|
message = (
|
||||||
f'Статус заявки "{request_obj.get_type_display()}" '
|
f'Статус заявки "{request_obj.get_type_display()}" '
|
||||||
f'изменен: {old_status} → {new_status}'
|
f"изменен: {old_status} → {new_status}"
|
||||||
)
|
)
|
||||||
|
|
||||||
NotificationService.create_notification(
|
NotificationService.create_notification(
|
||||||
@ -316,14 +341,14 @@ def notify_status_change(request_obj, old_status, new_status):
|
|||||||
title=title,
|
title=title,
|
||||||
message=message,
|
message=message,
|
||||||
content_object=request_obj,
|
content_object=request_obj,
|
||||||
action_url=f'/requests/{request_obj.id}/',
|
action_url=f"/requests/{request_obj.id}/",
|
||||||
metadata={
|
metadata={
|
||||||
'request_id': request_obj.id,
|
"request_id": request_obj.id,
|
||||||
'request_type': request_obj.type,
|
"request_type": request_obj.type,
|
||||||
'old_status': old_status,
|
"old_status": old_status,
|
||||||
'new_status': new_status,
|
"new_status": new_status,
|
||||||
'approver_id': (
|
"approver_id": (
|
||||||
request_obj.approver.id if request_obj.approver else None
|
request_obj.approver.id if request_obj.approver else None
|
||||||
),
|
),
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@ -153,7 +153,7 @@ def can_process_requests(user) -> bool:
|
|||||||
return True
|
return True
|
||||||
if is_department_head(user):
|
if is_department_head(user):
|
||||||
return True
|
return True
|
||||||
if user.has_perm('requests_app.can_process_requests'):
|
if user.has_perm("requests_app.can_process_requests"):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -351,7 +351,10 @@ def request_process(request, pk):
|
|||||||
req.refresh_from_db()
|
req.refresh_from_db()
|
||||||
|
|
||||||
# Проверка: не-staff должен быть получателем заявки
|
# Проверка: не-staff должен быть получателем заявки
|
||||||
if not is_hr(request.user) and not req.recipients.filter(id=request.user.id).exists():
|
if (
|
||||||
|
not is_hr(request.user)
|
||||||
|
and not req.recipients.filter(id=request.user.id).exists()
|
||||||
|
):
|
||||||
raise PermissionDenied("Вы не являетесь получателем этой заявки.")
|
raise PermissionDenied("Вы не являетесь получателем этой заявки.")
|
||||||
|
|
||||||
post = request.POST.copy()
|
post = request.POST.copy()
|
||||||
@ -489,6 +492,3 @@ def request_comment_delete(request, pk, comment_id):
|
|||||||
|
|
||||||
default_url = reverse(_detail_or_process_name(request.user), args=[pk])
|
default_url = reverse(_detail_or_process_name(request.user), args=[pk])
|
||||||
return redirect(_safe_back_url(request, default_url))
|
return redirect(_safe_back_url(request, default_url))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
* @description Обработчик CRUD операций для заявлений через API
|
* @description Обработчик CRUD операций для заявлений через API
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { RecipientPicker } from './recipientPicker.js';
|
import { RecipientPicker } from "./recipientPicker.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Инициализирует обработчики CRUD операций для заявлений
|
* Инициализирует обработчики CRUD операций для заявлений
|
||||||
@ -14,43 +14,41 @@ import { RecipientPicker } from './recipientPicker.js';
|
|||||||
* @returns {Object} API с методом destroy
|
* @returns {Object} API с методом destroy
|
||||||
*/
|
*/
|
||||||
export function initRequestCrudHandler(options) {
|
export function initRequestCrudHandler(options) {
|
||||||
const {
|
const { apiListUrl, apiDetailBase, headers = {} } = options;
|
||||||
apiListUrl,
|
|
||||||
apiDetailBase,
|
|
||||||
headers = {}
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
// Формы
|
// Формы
|
||||||
const createForm = document.getElementById('reqCreateForm');
|
const createForm = document.getElementById("reqCreateForm");
|
||||||
const editForm = document.getElementById('reqEditForm');
|
const editForm = document.getElementById("reqEditForm");
|
||||||
const deleteForm = document.getElementById('reqDeleteForm');
|
const deleteForm = document.getElementById("reqDeleteForm");
|
||||||
const approveForm = document.getElementById('reqApproveForm');
|
const approveForm = document.getElementById("reqApproveForm");
|
||||||
const rejectForm = document.getElementById('reqRejectForm');
|
const rejectForm = document.getElementById("reqRejectForm");
|
||||||
const cancelForm = document.getElementById('reqCancelForm');
|
const cancelForm = document.getElementById("reqCancelForm");
|
||||||
const commentForm = document.getElementById('reqCommentForm');
|
const commentForm = document.getElementById("reqCommentForm");
|
||||||
|
|
||||||
if (!createForm) {
|
if (!createForm) {
|
||||||
console.warn('initRequestCrudHandler: required forms not found');
|
console.warn("initRequestCrudHandler: required forms not found");
|
||||||
return { destroy: () => {} };
|
return { destroy: () => {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Инициализация RecipientPicker для создания
|
// Инициализация RecipientPicker для создания
|
||||||
let createRecipientPicker = null;
|
let createRecipientPicker = null;
|
||||||
const createPickerContainer = document.getElementById('createRecipientPicker');
|
const createPickerContainer = document.getElementById(
|
||||||
|
"createRecipientPicker"
|
||||||
|
);
|
||||||
if (createPickerContainer) {
|
if (createPickerContainer) {
|
||||||
createRecipientPicker = new RecipientPicker(createPickerContainer, {
|
createRecipientPicker = new RecipientPicker(createPickerContainer, {
|
||||||
apiUsersUrl: '/api/v1/employees/',
|
apiUsersUrl: "/api/v1/employees/",
|
||||||
apiDepartmentsUrl: '/api/v1/departments/'
|
apiDepartmentsUrl: "/api/v1/departments/",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Инициализация RecipientPicker для редактирования
|
// Инициализация RecipientPicker для редактирования
|
||||||
let editRecipientPicker = null;
|
let editRecipientPicker = null;
|
||||||
const editPickerContainer = document.getElementById('editRecipientPicker');
|
const editPickerContainer = document.getElementById("editRecipientPicker");
|
||||||
if (editPickerContainer) {
|
if (editPickerContainer) {
|
||||||
editRecipientPicker = new RecipientPicker(editPickerContainer, {
|
editRecipientPicker = new RecipientPicker(editPickerContainer, {
|
||||||
apiUsersUrl: '/api/v1/employees/',
|
apiUsersUrl: "/api/v1/employees/",
|
||||||
apiDepartmentsUrl: '/api/v1/departments/'
|
apiDepartmentsUrl: "/api/v1/departments/",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,13 +58,13 @@ export function initRequestCrudHandler(options) {
|
|||||||
async function handleCreate(e) {
|
async function handleCreate(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const saveAs = e.submitter?.value || 'submit'; // 'draft' or 'submit'
|
const saveAs = e.submitter?.value || "submit"; // 'draft' or 'submit'
|
||||||
const formData = new FormData(createForm);
|
const formData = new FormData(createForm);
|
||||||
|
|
||||||
// Добавляем данные получателей из RecipientPicker
|
// Добавляем данные получателей из RecipientPicker
|
||||||
if (createRecipientPicker) {
|
if (createRecipientPicker) {
|
||||||
// Валидация: для обычной отправки должны быть получатели или отделы
|
// Валидация: для обычной отправки должны быть получатели или отделы
|
||||||
if (saveAs === 'submit') {
|
if (saveAs === "submit") {
|
||||||
if (!createRecipientPicker.validate()) {
|
if (!createRecipientPicker.validate()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -75,8 +73,8 @@ export function initRequestCrudHandler(options) {
|
|||||||
const recipients = createRecipientPicker.getValues();
|
const recipients = createRecipientPicker.getValues();
|
||||||
if (recipients.recipient_ids && window.currentUserId) {
|
if (recipients.recipient_ids && window.currentUserId) {
|
||||||
if (recipients.recipient_ids.includes(window.currentUserId)) {
|
if (recipients.recipient_ids.includes(window.currentUserId)) {
|
||||||
alert('Вы не можете отправить заявление самому себе');
|
alert("Вы не можете отправить заявление самому себе");
|
||||||
createRecipientPicker.setError('Вы не можете быть получателем');
|
createRecipientPicker.setError("Вы не можете быть получателем");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -86,67 +84,70 @@ export function initRequestCrudHandler(options) {
|
|||||||
|
|
||||||
// Добавляем department_ids
|
// Добавляем department_ids
|
||||||
if (recipients.department_ids && recipients.department_ids.length > 0) {
|
if (recipients.department_ids && recipients.department_ids.length > 0) {
|
||||||
recipients.department_ids.forEach(id => {
|
recipients.department_ids.forEach((id) => {
|
||||||
formData.append('department_ids', id);
|
formData.append("department_ids", id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Добавляем recipient_ids
|
// Добавляем recipient_ids
|
||||||
if (recipients.recipient_ids && recipients.recipient_ids.length > 0) {
|
if (recipients.recipient_ids && recipients.recipient_ids.length > 0) {
|
||||||
recipients.recipient_ids.forEach(id => {
|
recipients.recipient_ids.forEach((id) => {
|
||||||
formData.append('recipient_ids', id);
|
formData.append("recipient_ids", id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Добавляем cc_user_ids
|
// Добавляем cc_user_ids
|
||||||
if (recipients.cc_user_ids && recipients.cc_user_ids.length > 0) {
|
if (recipients.cc_user_ids && recipients.cc_user_ids.length > 0) {
|
||||||
recipients.cc_user_ids.forEach(id => {
|
recipients.cc_user_ids.forEach((id) => {
|
||||||
formData.append('cc_user_ids', id);
|
formData.append("cc_user_ids", id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Добавляем sent_to_all_department
|
// Добавляем sent_to_all_department
|
||||||
if (recipients.sent_to_all_department) {
|
if (recipients.sent_to_all_department) {
|
||||||
formData.set('sent_to_all_department', 'true');
|
formData.set("sent_to_all_department", "true");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Удаляем пустые поля для черновика
|
// Удаляем пустые поля для черновика
|
||||||
if (saveAs === 'draft') {
|
if (saveAs === "draft") {
|
||||||
for (const [key, value] of Array.from(formData.entries())) {
|
for (const [key, value] of Array.from(formData.entries())) {
|
||||||
if (!value || value === '') {
|
if (!value || value === "") {
|
||||||
formData.delete(key);
|
formData.delete(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = saveAs === 'draft' ? `${apiListUrl}?save_as=draft` : apiListUrl;
|
const url =
|
||||||
|
saveAs === "draft" ? `${apiListUrl}?save_as=draft` : apiListUrl;
|
||||||
|
|
||||||
// Логируем данные для отладки
|
// Логируем данные для отладки
|
||||||
console.log('[requestCrudHandler] Creating request with data:');
|
console.log("[requestCrudHandler] Creating request with data:");
|
||||||
for (const [key, value] of formData.entries()) {
|
for (const [key, value] of formData.entries()) {
|
||||||
console.log(` ${key}:`, value);
|
console.log(` ${key}:`, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: headers,
|
headers: headers,
|
||||||
body: formData
|
body: formData,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
console.error('[requestCrudHandler] Validation error:', error);
|
console.error("[requestCrudHandler] Validation error:", error);
|
||||||
|
|
||||||
// Показываем детали ошибки
|
// Показываем детали ошибки
|
||||||
let errorMessage = 'Не удалось создать заявление:\n';
|
let errorMessage = "Не удалось создать заявление:\n";
|
||||||
if (error.detail) {
|
if (error.detail) {
|
||||||
errorMessage += error.detail;
|
errorMessage += error.detail;
|
||||||
} else if (typeof error === 'object') {
|
} else if (typeof error === "object") {
|
||||||
// Форматируем ошибки валидации
|
// Форматируем ошибки валидации
|
||||||
for (const [field, messages] of Object.entries(error)) {
|
for (const [field, messages] of Object.entries(error)) {
|
||||||
const fieldMessages = Array.isArray(messages) ? messages.join(', ') : messages;
|
const fieldMessages = Array.isArray(messages)
|
||||||
|
? messages.join(", ")
|
||||||
|
: messages;
|
||||||
errorMessage += `\n${field}: ${fieldMessages}`;
|
errorMessage += `\n${field}: ${fieldMessages}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -157,7 +158,10 @@ export function initRequestCrudHandler(options) {
|
|||||||
await response.json();
|
await response.json();
|
||||||
|
|
||||||
// Показываем сообщение и перезагружаем
|
// Показываем сообщение и перезагружаем
|
||||||
const message = saveAs === 'draft' ? 'Черновик сохранён.' : 'Заявление отправлено на рассмотрение.';
|
const message =
|
||||||
|
saveAs === "draft"
|
||||||
|
? "Черновик сохранён."
|
||||||
|
: "Заявление отправлено на рассмотрение.";
|
||||||
alert(message); // TODO: заменить на toast-уведомление
|
alert(message); // TODO: заменить на toast-уведомление
|
||||||
|
|
||||||
// Очищаем форму и picker
|
// Очищаем форму и picker
|
||||||
@ -168,7 +172,7 @@ export function initRequestCrudHandler(options) {
|
|||||||
|
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('Не удалось создать заявление: ' + error.message);
|
alert("Не удалось создать заявление: " + error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,13 +183,13 @@ export function initRequestCrudHandler(options) {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const reqId = editForm.elements.id.value;
|
const reqId = editForm.elements.id.value;
|
||||||
const saveAs = e.submitter?.value || 'submit';
|
const saveAs = e.submitter?.value || "submit";
|
||||||
const formData = new FormData(editForm);
|
const formData = new FormData(editForm);
|
||||||
|
|
||||||
// Добавляем данные получателей из RecipientPicker
|
// Добавляем данные получателей из RecipientPicker
|
||||||
if (editRecipientPicker) {
|
if (editRecipientPicker) {
|
||||||
// Валидация: для обычной отправки должны быть получатели или отделы
|
// Валидация: для обычной отправки должны быть получатели или отделы
|
||||||
if (saveAs === 'submit') {
|
if (saveAs === "submit") {
|
||||||
if (!editRecipientPicker.validate()) {
|
if (!editRecipientPicker.validate()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -194,8 +198,8 @@ export function initRequestCrudHandler(options) {
|
|||||||
const recipients = editRecipientPicker.getValues();
|
const recipients = editRecipientPicker.getValues();
|
||||||
if (recipients.recipient_ids && window.currentUserId) {
|
if (recipients.recipient_ids && window.currentUserId) {
|
||||||
if (recipients.recipient_ids.includes(window.currentUserId)) {
|
if (recipients.recipient_ids.includes(window.currentUserId)) {
|
||||||
alert('Вы не можете отправить заявление самому себе');
|
alert("Вы не можете отправить заявление самому себе");
|
||||||
editRecipientPicker.setError('Вы не можете быть получателем');
|
editRecipientPicker.setError("Вы не можете быть получателем");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -205,60 +209,60 @@ export function initRequestCrudHandler(options) {
|
|||||||
|
|
||||||
// Добавляем department_ids
|
// Добавляем department_ids
|
||||||
if (recipients.department_ids && recipients.department_ids.length > 0) {
|
if (recipients.department_ids && recipients.department_ids.length > 0) {
|
||||||
recipients.department_ids.forEach(id => {
|
recipients.department_ids.forEach((id) => {
|
||||||
formData.append('department_ids', id);
|
formData.append("department_ids", id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Добавляем recipient_ids
|
// Добавляем recipient_ids
|
||||||
if (recipients.recipient_ids && recipients.recipient_ids.length > 0) {
|
if (recipients.recipient_ids && recipients.recipient_ids.length > 0) {
|
||||||
recipients.recipient_ids.forEach(id => {
|
recipients.recipient_ids.forEach((id) => {
|
||||||
formData.append('recipient_ids', id);
|
formData.append("recipient_ids", id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Добавляем cc_user_ids
|
// Добавляем cc_user_ids
|
||||||
if (recipients.cc_user_ids && recipients.cc_user_ids.length > 0) {
|
if (recipients.cc_user_ids && recipients.cc_user_ids.length > 0) {
|
||||||
recipients.cc_user_ids.forEach(id => {
|
recipients.cc_user_ids.forEach((id) => {
|
||||||
formData.append('cc_user_ids', id);
|
formData.append("cc_user_ids", id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Добавляем sent_to_all_department
|
// Добавляем sent_to_all_department
|
||||||
if (recipients.sent_to_all_department) {
|
if (recipients.sent_to_all_department) {
|
||||||
formData.set('sent_to_all_department', 'true');
|
formData.set("sent_to_all_department", "true");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Удаляем пустые поля
|
// Удаляем пустые поля
|
||||||
for (const [key, value] of Array.from(formData.entries())) {
|
for (const [key, value] of Array.from(formData.entries())) {
|
||||||
if (!value || value === '') {
|
if (!value || value === "") {
|
||||||
formData.delete(key);
|
formData.delete(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let url = `${apiDetailBase}${reqId}/`;
|
let url = `${apiDetailBase}${reqId}/`;
|
||||||
if (saveAs === 'draft' || saveAs === 'submit') {
|
if (saveAs === "draft" || saveAs === "submit") {
|
||||||
url += `?save_as=${saveAs}`;
|
url += `?save_as=${saveAs}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'PATCH',
|
method: "PATCH",
|
||||||
headers: headers,
|
headers: headers,
|
||||||
body: formData
|
body: formData,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
throw new Error(error.detail || 'HTTP ' + response.status);
|
throw new Error(error.detail || "HTTP " + response.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
await response.json();
|
await response.json();
|
||||||
alert('Заявление обновлено.');
|
alert("Заявление обновлено.");
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('Не удалось обновить заявление: ' + error.message);
|
alert("Не удалось обновить заявление: " + error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -270,25 +274,25 @@ export function initRequestCrudHandler(options) {
|
|||||||
|
|
||||||
const reqId = deleteForm.elements.id.value;
|
const reqId = deleteForm.elements.id.value;
|
||||||
|
|
||||||
if (!confirm('Вы уверены, что хотите удалить это заявление?')) {
|
if (!confirm("Вы уверены, что хотите удалить это заявление?")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${apiDetailBase}${reqId}/`, {
|
const response = await fetch(`${apiDetailBase}${reqId}/`, {
|
||||||
method: 'DELETE',
|
method: "DELETE",
|
||||||
headers: headers
|
headers: headers,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok && response.status !== 204) {
|
if (!response.ok && response.status !== 204) {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
throw new Error(error.detail || 'HTTP ' + response.status);
|
throw new Error(error.detail || "HTTP " + response.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
alert('Заявление удалено.');
|
alert("Заявление удалено.");
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('Не удалось удалить заявление: ' + error.message);
|
alert("Не удалось удалить заявление: " + error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -302,24 +306,24 @@ export function initRequestCrudHandler(options) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${apiDetailBase}${reqId}/approve/`, {
|
const response = await fetch(`${apiDetailBase}${reqId}/approve/`, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
...headers,
|
...headers,
|
||||||
'Content-Type': 'application/json'
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({})
|
body: JSON.stringify({}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
throw new Error(error.detail || 'HTTP ' + response.status);
|
throw new Error(error.detail || "HTTP " + response.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
await response.json();
|
await response.json();
|
||||||
alert('Заявление одобрено.');
|
alert("Заявление одобрено.");
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('Не удалось одобрить заявление: ' + error.message);
|
alert("Не удалось одобрить заявление: " + error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -333,24 +337,24 @@ export function initRequestCrudHandler(options) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${apiDetailBase}${reqId}/reject/`, {
|
const response = await fetch(`${apiDetailBase}${reqId}/reject/`, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
...headers,
|
...headers,
|
||||||
'Content-Type': 'application/json'
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({})
|
body: JSON.stringify({}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
throw new Error(error.detail || 'HTTP ' + response.status);
|
throw new Error(error.detail || "HTTP " + response.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
await response.json();
|
await response.json();
|
||||||
alert('Заявление отклонено.');
|
alert("Заявление отклонено.");
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('Не удалось отклонить заявление: ' + error.message);
|
alert("Не удалось отклонить заявление: " + error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -362,30 +366,30 @@ export function initRequestCrudHandler(options) {
|
|||||||
|
|
||||||
const reqId = cancelForm.elements.id.value;
|
const reqId = cancelForm.elements.id.value;
|
||||||
|
|
||||||
if (!confirm('Вы уверены, что хотите отменить это заявление?')) {
|
if (!confirm("Вы уверены, что хотите отменить это заявление?")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${apiDetailBase}${reqId}/cancel/`, {
|
const response = await fetch(`${apiDetailBase}${reqId}/cancel/`, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
...headers,
|
...headers,
|
||||||
'Content-Type': 'application/json'
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({})
|
body: JSON.stringify({}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
throw new Error(error.detail || 'HTTP ' + response.status);
|
throw new Error(error.detail || "HTTP " + response.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
await response.json();
|
await response.json();
|
||||||
alert('Заявление отменено.');
|
alert("Заявление отменено.");
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('Не удалось отменить заявление: ' + error.message);
|
alert("Не удалось отменить заявление: " + error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -396,56 +400,56 @@ export function initRequestCrudHandler(options) {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const reqId = commentForm.elements.id.value;
|
const reqId = commentForm.elements.id.value;
|
||||||
const text = (commentForm.elements.text.value || '').trim();
|
const text = (commentForm.elements.text.value || "").trim();
|
||||||
|
|
||||||
if (!text) {
|
if (!text) {
|
||||||
alert('Комментарий не может быть пустым.');
|
alert("Комментарий не может быть пустым.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${apiDetailBase}${reqId}/comments/`, {
|
const response = await fetch(`${apiDetailBase}${reqId}/comments/`, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
...headers,
|
...headers,
|
||||||
'Content-Type': 'application/json'
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ text })
|
body: JSON.stringify({ text }),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok && response.status !== 201) {
|
if (!response.ok && response.status !== 201) {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
throw new Error(error.detail || 'HTTP ' + response.status);
|
throw new Error(error.detail || "HTTP " + response.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
await response.json();
|
await response.json();
|
||||||
alert('Комментарий добавлен.');
|
alert("Комментарий добавлен.");
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('Не удалось добавить комментарий: ' + error.message);
|
alert("Не удалось добавить комментарий: " + error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Установка обработчиков
|
// Установка обработчиков
|
||||||
createForm?.addEventListener('submit', handleCreate);
|
createForm?.addEventListener("submit", handleCreate);
|
||||||
editForm?.addEventListener('submit', handleEdit);
|
editForm?.addEventListener("submit", handleEdit);
|
||||||
deleteForm?.addEventListener('submit', handleDelete);
|
deleteForm?.addEventListener("submit", handleDelete);
|
||||||
approveForm?.addEventListener('submit', handleApprove);
|
approveForm?.addEventListener("submit", handleApprove);
|
||||||
rejectForm?.addEventListener('submit', handleReject);
|
rejectForm?.addEventListener("submit", handleReject);
|
||||||
cancelForm?.addEventListener('submit', handleCancel);
|
cancelForm?.addEventListener("submit", handleCancel);
|
||||||
commentForm?.addEventListener('submit', handleComment);
|
commentForm?.addEventListener("submit", handleComment);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Функция для удаления всех обработчиков
|
* Функция для удаления всех обработчиков
|
||||||
*/
|
*/
|
||||||
function destroy() {
|
function destroy() {
|
||||||
createForm?.removeEventListener('submit', handleCreate);
|
createForm?.removeEventListener("submit", handleCreate);
|
||||||
editForm?.removeEventListener('submit', handleEdit);
|
editForm?.removeEventListener("submit", handleEdit);
|
||||||
deleteForm?.removeEventListener('submit', handleDelete);
|
deleteForm?.removeEventListener("submit", handleDelete);
|
||||||
approveForm?.removeEventListener('submit', handleApprove);
|
approveForm?.removeEventListener("submit", handleApprove);
|
||||||
rejectForm?.removeEventListener('submit', handleReject);
|
rejectForm?.removeEventListener("submit", handleReject);
|
||||||
cancelForm?.removeEventListener('submit', handleCancel);
|
cancelForm?.removeEventListener("submit", handleCancel);
|
||||||
commentForm?.removeEventListener('submit', handleComment);
|
commentForm?.removeEventListener("submit", handleComment);
|
||||||
|
|
||||||
// Очистка RecipientPicker
|
// Очистка RecipientPicker
|
||||||
if (createRecipientPicker) {
|
if (createRecipientPicker) {
|
||||||
@ -456,16 +460,16 @@ export function initRequestCrudHandler(options) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Request CRUD handler initialized');
|
console.log("Request CRUD handler initialized");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
destroy,
|
destroy,
|
||||||
createRecipientPicker,
|
createRecipientPicker,
|
||||||
editRecipientPicker
|
editRecipientPicker,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Экспорт для совместимости с неModular кодом
|
// Экспорт для совместимости с неModular кодом
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== "undefined") {
|
||||||
window.initRequestCrudHandler = initRequestCrudHandler;
|
window.initRequestCrudHandler = initRequestCrudHandler;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,30 +19,37 @@ export function initRequestListHandler(options) {
|
|||||||
detailUrlTemplate,
|
detailUrlTemplate,
|
||||||
userId,
|
userId,
|
||||||
canProcess,
|
canProcess,
|
||||||
headers = {}
|
headers = {},
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
const listElement = document.getElementById('reqListContent');
|
const listElement = document.getElementById("reqListContent");
|
||||||
const searchInput = document.getElementById('reqFilter');
|
const searchInput = document.getElementById("reqFilter");
|
||||||
|
|
||||||
if (!listElement) {
|
if (!listElement) {
|
||||||
console.warn('initRequestListHandler: #reqListContent not found');
|
console.warn("initRequestListHandler: #reqListContent not found");
|
||||||
return { load: () => {}, destroy: () => {} };
|
return { load: () => {}, destroy: () => {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Текущее состояние
|
// Текущее состояние
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
let currentView = urlParams.get('view') || ''; // 'mine' | 'addressed' | ''
|
let currentView = urlParams.get("view") || ""; // 'mine' | 'addressed' | ''
|
||||||
let currentType = urlParams.get('type') || '';
|
let currentType = urlParams.get("type") || "";
|
||||||
let currentStatus = urlParams.get('status') || '';
|
let currentStatus = urlParams.get("status") || "";
|
||||||
let searchQuery = ''; // Поисковый запрос
|
let searchQuery = ""; // Поисковый запрос
|
||||||
let allRequests = []; // Все загруженные заявления
|
let allRequests = []; // Все загруженные заявления
|
||||||
let loading = false;
|
let loading = false;
|
||||||
let hasMore = true;
|
let hasMore = true;
|
||||||
let nextUrl = null;
|
let nextUrl = null;
|
||||||
let totalCount = 0;
|
let totalCount = 0;
|
||||||
|
|
||||||
console.log('requestListHandler: init with view =', currentView, 'type =', currentType, 'status =', currentStatus);
|
console.log(
|
||||||
|
"requestListHandler: init with view =",
|
||||||
|
currentView,
|
||||||
|
"type =",
|
||||||
|
currentType,
|
||||||
|
"status =",
|
||||||
|
currentStatus
|
||||||
|
);
|
||||||
|
|
||||||
// Intersection Observer для бесконечной прокрутки
|
// Intersection Observer для бесконечной прокрутки
|
||||||
let observerTarget = null;
|
let observerTarget = null;
|
||||||
@ -50,14 +57,15 @@ export function initRequestListHandler(options) {
|
|||||||
|
|
||||||
// Мобильная оптимизация: слушаем сетевые события
|
// Мобильная оптимизация: слушаем сетевые события
|
||||||
let isOnline = navigator.onLine;
|
let isOnline = navigator.onLine;
|
||||||
window.addEventListener('online', () => {
|
window.addEventListener("online", () => {
|
||||||
isOnline = true;
|
isOnline = true;
|
||||||
console.log('Network restored');
|
console.log("Network restored");
|
||||||
});
|
});
|
||||||
window.addEventListener('offline', () => {
|
window.addEventListener("offline", () => {
|
||||||
isOnline = false;
|
isOnline = false;
|
||||||
console.log('Network lost');
|
console.log("Network lost");
|
||||||
listElement.innerHTML = '<div class="p-4 text-center text-warning">Потеряна связь с интернетом</div>';
|
listElement.innerHTML =
|
||||||
|
'<div class="p-4 text-center text-warning">Потеряна связь с интернетом</div>';
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -67,14 +75,15 @@ export function initRequestListHandler(options) {
|
|||||||
*/
|
*/
|
||||||
async function loadRequests(append = false) {
|
async function loadRequests(append = false) {
|
||||||
if (loading || (append && !hasMore)) {
|
if (loading || (append && !hasMore)) {
|
||||||
console.log('loadRequests: skipped', { loading, append, hasMore });
|
console.log("loadRequests: skipped", { loading, append, hasMore });
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверка сети на мобильных
|
// Проверка сети на мобильных
|
||||||
if (!isOnline) {
|
if (!isOnline) {
|
||||||
console.warn('loadRequests: offline');
|
console.warn("loadRequests: offline");
|
||||||
listElement.innerHTML = '<div class="p-4 text-center text-warning">Нет интернет-соединения. Проверьте подключение.</div>';
|
listElement.innerHTML =
|
||||||
|
'<div class="p-4 text-center text-warning">Нет интернет-соединения. Проверьте подключение.</div>';
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,55 +94,57 @@ export function initRequestListHandler(options) {
|
|||||||
let url;
|
let url;
|
||||||
if (append && nextUrl) {
|
if (append && nextUrl) {
|
||||||
url = nextUrl;
|
url = nextUrl;
|
||||||
console.log('loadRequests: using nextUrl', url);
|
console.log("loadRequests: using nextUrl", url);
|
||||||
} else if (append && !nextUrl) {
|
} else if (append && !nextUrl) {
|
||||||
console.warn('loadRequests: append=true but no nextUrl, stopping');
|
console.warn("loadRequests: append=true but no nextUrl, stopping");
|
||||||
loading = false;
|
loading = false;
|
||||||
hideLoadingSpinner();
|
hideLoadingSpinner();
|
||||||
return [];
|
return [];
|
||||||
} else {
|
} else {
|
||||||
// Формируем URL с параметрами фильтров
|
// Формируем URL с параметрами фильтров
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (currentView === 'mine') {
|
if (currentView === "mine") {
|
||||||
params.append('view', 'mine');
|
params.append("view", "mine");
|
||||||
} else if (currentView === 'addressed') {
|
} else if (currentView === "addressed") {
|
||||||
params.append('addressed_to_me', 'true');
|
params.append("addressed_to_me", "true");
|
||||||
}
|
}
|
||||||
if (currentType) {
|
if (currentType) {
|
||||||
params.append('type', currentType);
|
params.append("type", currentType);
|
||||||
}
|
}
|
||||||
if (currentStatus) {
|
if (currentStatus) {
|
||||||
params.append('status', currentStatus);
|
params.append("status", currentStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
url = `${apiListUrl}?${params}`;
|
url = `${apiListUrl}?${params}`;
|
||||||
console.log('loadRequests: initial load', url);
|
console.log("loadRequests: initial load", url);
|
||||||
}
|
}
|
||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
// На мобильных устройствах увеличиваем таймаут до 15 секунд
|
// На мобильных устройствах увеличиваем таймаут до 15 секунд
|
||||||
const timeoutMs = /iPhone|iPad|Android|Mobile/.test(navigator.userAgent) ? 15000 : 10000;
|
const timeoutMs = /iPhone|iPad|Android|Mobile/.test(navigator.userAgent)
|
||||||
|
? 15000
|
||||||
|
: 10000;
|
||||||
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
headers,
|
headers,
|
||||||
signal: controller.signal
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('HTTP ' + response.status);
|
throw new Error("HTTP " + response.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const newRequests = data.results || [];
|
const newRequests = data.results || [];
|
||||||
|
|
||||||
console.log('loadRequests: received', {
|
console.log("loadRequests: received", {
|
||||||
newCount: newRequests.length,
|
newCount: newRequests.length,
|
||||||
totalCount: data.count,
|
totalCount: data.count,
|
||||||
hasNext: !!data.next,
|
hasNext: !!data.next,
|
||||||
currentRequests: allRequests.length
|
currentRequests: allRequests.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Сохраняем метаданные
|
// Сохраняем метаданные
|
||||||
@ -143,10 +154,10 @@ export function initRequestListHandler(options) {
|
|||||||
|
|
||||||
if (append) {
|
if (append) {
|
||||||
allRequests = [...allRequests, ...newRequests];
|
allRequests = [...allRequests, ...newRequests];
|
||||||
console.log('loadRequests: appended, total now:', allRequests.length);
|
console.log("loadRequests: appended, total now:", allRequests.length);
|
||||||
} else {
|
} else {
|
||||||
allRequests = newRequests;
|
allRequests = newRequests;
|
||||||
console.log('loadRequests: replaced, total now:', allRequests.length);
|
console.log("loadRequests: replaced, total now:", allRequests.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
loading = false;
|
loading = false;
|
||||||
@ -154,16 +165,16 @@ export function initRequestListHandler(options) {
|
|||||||
|
|
||||||
return newRequests;
|
return newRequests;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка загрузки заявлений:', error);
|
console.error("Ошибка загрузки заявлений:", error);
|
||||||
loading = false;
|
loading = false;
|
||||||
hideLoadingSpinner();
|
hideLoadingSpinner();
|
||||||
|
|
||||||
// Более информативное сообщение об ошибке
|
// Более информативное сообщение об ошибке
|
||||||
let errorMsg = 'Не удалось загрузить заявления';
|
let errorMsg = "Не удалось загрузить заявления";
|
||||||
if (error.name === 'AbortError') {
|
if (error.name === "AbortError") {
|
||||||
errorMsg = 'Время ожидания истекло. Проверьте интернет-соединение.';
|
errorMsg = "Время ожидания истекло. Проверьте интернет-соединение.";
|
||||||
} else if (!navigator.onLine) {
|
} else if (!navigator.onLine) {
|
||||||
errorMsg = 'Нет интернет-соединения.';
|
errorMsg = "Нет интернет-соединения.";
|
||||||
}
|
}
|
||||||
|
|
||||||
listElement.innerHTML = `
|
listElement.innerHTML = `
|
||||||
@ -180,11 +191,11 @@ export function initRequestListHandler(options) {
|
|||||||
* Показать спиннер загрузки
|
* Показать спиннер загрузки
|
||||||
*/
|
*/
|
||||||
function showLoadingSpinner() {
|
function showLoadingSpinner() {
|
||||||
const existing = listElement.querySelector('.loading-spinner');
|
const existing = listElement.querySelector(".loading-spinner");
|
||||||
if (existing) return;
|
if (existing) return;
|
||||||
|
|
||||||
const spinner = document.createElement('div');
|
const spinner = document.createElement("div");
|
||||||
spinner.className = 'loading-spinner text-center py-4';
|
spinner.className = "loading-spinner text-center py-4";
|
||||||
spinner.innerHTML = `
|
spinner.innerHTML = `
|
||||||
<div class="spinner-border text-primary" role="status">
|
<div class="spinner-border text-primary" role="status">
|
||||||
<span class="visually-hidden">Загрузка...</span>
|
<span class="visually-hidden">Загрузка...</span>
|
||||||
@ -197,12 +208,12 @@ export function initRequestListHandler(options) {
|
|||||||
* Скрыть спиннер загрузки
|
* Скрыть спиннер загрузки
|
||||||
*/
|
*/
|
||||||
function hideLoadingSpinner() {
|
function hideLoadingSpinner() {
|
||||||
const spinner = listElement.querySelector('.loading-spinner');
|
const spinner = listElement.querySelector(".loading-spinner");
|
||||||
if (spinner) {
|
if (spinner) {
|
||||||
spinner.remove();
|
spinner.remove();
|
||||||
}
|
}
|
||||||
// Удаляем начальный спиннер из шаблона
|
// Удаляем начальный спиннер из шаблона
|
||||||
const initialSpinner = document.getElementById('reqListLoading');
|
const initialSpinner = document.getElementById("reqListLoading");
|
||||||
if (initialSpinner) {
|
if (initialSpinner) {
|
||||||
initialSpinner.remove();
|
initialSpinner.remove();
|
||||||
}
|
}
|
||||||
@ -212,18 +223,18 @@ export function initRequestListHandler(options) {
|
|||||||
* Загрузка следующей порции заявлений
|
* Загрузка следующей порции заявлений
|
||||||
*/
|
*/
|
||||||
async function loadMore() {
|
async function loadMore() {
|
||||||
console.log('loadMore: triggered', {
|
console.log("loadMore: triggered", {
|
||||||
hasMore,
|
hasMore,
|
||||||
loading,
|
loading,
|
||||||
nextUrl,
|
nextUrl,
|
||||||
currentCount: allRequests.length
|
currentCount: allRequests.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newRequests = await loadRequests(true); // append = true
|
const newRequests = await loadRequests(true); // append = true
|
||||||
renderRequests(newRequests, true); // append = true
|
renderRequests(newRequests, true); // append = true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load more requests:', error);
|
console.error("Failed to load more requests:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,13 +246,13 @@ export function initRequestListHandler(options) {
|
|||||||
|
|
||||||
// Создаем observer с адаптивными параметрами для мобильных
|
// Создаем observer с адаптивными параметрами для мобильных
|
||||||
const isMobile = /iPhone|iPad|Android|Mobile/.test(navigator.userAgent);
|
const isMobile = /iPhone|iPad|Android|Mobile/.test(navigator.userAgent);
|
||||||
const rootMargin = isMobile ? '200px' : '100px'; // На мобильных загружаем раньше
|
const rootMargin = isMobile ? "200px" : "100px"; // На мобильных загружаем раньше
|
||||||
|
|
||||||
observer = new IntersectionObserver(
|
observer = new IntersectionObserver(
|
||||||
(entries) => {
|
(entries) => {
|
||||||
entries.forEach(entry => {
|
entries.forEach((entry) => {
|
||||||
if (entry.isIntersecting && hasMore && !loading) {
|
if (entry.isIntersecting && hasMore && !loading) {
|
||||||
console.log('Intersection observer: triggering loadMore');
|
console.log("Intersection observer: triggering loadMore");
|
||||||
loadMore();
|
loadMore();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -249,7 +260,7 @@ export function initRequestListHandler(options) {
|
|||||||
{
|
{
|
||||||
root: null,
|
root: null,
|
||||||
rootMargin,
|
rootMargin,
|
||||||
threshold: 0.01
|
threshold: 0.01,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -266,11 +277,15 @@ export function initRequestListHandler(options) {
|
|||||||
if (!searchQuery.trim()) return items;
|
if (!searchQuery.trim()) return items;
|
||||||
|
|
||||||
const query = searchQuery.toLowerCase().trim();
|
const query = searchQuery.toLowerCase().trim();
|
||||||
return items.filter(req => {
|
return items.filter((req) => {
|
||||||
const title = (req.display_title || req.title || '').toLowerCase();
|
const title = (req.display_title || req.title || "").toLowerCase();
|
||||||
const comment = (req.comment || '').toLowerCase();
|
const comment = (req.comment || "").toLowerCase();
|
||||||
const employeeName = (req.employee?.full_name || '').toLowerCase();
|
const employeeName = (req.employee?.full_name || "").toLowerCase();
|
||||||
return title.includes(query) || comment.includes(query) || employeeName.includes(query);
|
return (
|
||||||
|
title.includes(query) ||
|
||||||
|
comment.includes(query) ||
|
||||||
|
employeeName.includes(query)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -282,7 +297,8 @@ export function initRequestListHandler(options) {
|
|||||||
function renderRequests(requests, append = false) {
|
function renderRequests(requests, append = false) {
|
||||||
if (!requests || requests.length === 0) {
|
if (!requests || requests.length === 0) {
|
||||||
if (!append) {
|
if (!append) {
|
||||||
listElement.innerHTML = '<div class="p-4 text-center text-secondary">Нет доступных заявлений.</div>';
|
listElement.innerHTML =
|
||||||
|
'<div class="p-4 text-center text-secondary">Нет доступных заявлений.</div>';
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -290,17 +306,18 @@ export function initRequestListHandler(options) {
|
|||||||
let items = filterBySearch(requests);
|
let items = filterBySearch(requests);
|
||||||
|
|
||||||
if (items.length === 0 && !append) {
|
if (items.length === 0 && !append) {
|
||||||
listElement.innerHTML = '<div class="p-4 text-center text-secondary">Нет заявлений, соответствующих фильтрам.</div>';
|
listElement.innerHTML =
|
||||||
|
'<div class="p-4 text-center text-secondary">Нет заявлений, соответствующих фильтрам.</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('renderRequests: rendering', {
|
console.log("renderRequests: rendering", {
|
||||||
count: items.length,
|
count: items.length,
|
||||||
append,
|
append,
|
||||||
totalInMemory: allRequests.length
|
totalInMemory: allRequests.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
const itemsHtml = items.map(req => renderRequestRow(req)).join('');
|
const itemsHtml = items.map((req) => renderRequestRow(req)).join("");
|
||||||
|
|
||||||
if (append) {
|
if (append) {
|
||||||
// Удаляем observer target перед добавлением
|
// Удаляем observer target перед добавлением
|
||||||
@ -308,20 +325,20 @@ export function initRequestListHandler(options) {
|
|||||||
observerTarget.remove();
|
observerTarget.remove();
|
||||||
}
|
}
|
||||||
// Добавляем новые карточки
|
// Добавляем новые карточки
|
||||||
listElement.insertAdjacentHTML('beforeend', itemsHtml);
|
listElement.insertAdjacentHTML("beforeend", itemsHtml);
|
||||||
console.log('renderRequests: appended to DOM');
|
console.log("renderRequests: appended to DOM");
|
||||||
// Возвращаем observer target обратно
|
// Возвращаем observer target обратно
|
||||||
if (observerTarget) {
|
if (observerTarget) {
|
||||||
listElement.appendChild(observerTarget);
|
listElement.appendChild(observerTarget);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
listElement.innerHTML = itemsHtml;
|
listElement.innerHTML = itemsHtml;
|
||||||
console.log('renderRequests: replaced DOM');
|
console.log("renderRequests: replaced DOM");
|
||||||
// Создаем observer target при первой загрузке
|
// Создаем observer target при первой загрузке
|
||||||
if (hasMore && !observerTarget) {
|
if (hasMore && !observerTarget) {
|
||||||
observerTarget = document.createElement('div');
|
observerTarget = document.createElement("div");
|
||||||
observerTarget.className = 'load-more-trigger';
|
observerTarget.className = "load-more-trigger";
|
||||||
observerTarget.style.height = '1px';
|
observerTarget.style.height = "1px";
|
||||||
listElement.appendChild(observerTarget);
|
listElement.appendChild(observerTarget);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -333,42 +350,57 @@ export function initRequestListHandler(options) {
|
|||||||
* @returns {string} HTML
|
* @returns {string} HTML
|
||||||
*/
|
*/
|
||||||
function renderRequestRow(req) {
|
function renderRequestRow(req) {
|
||||||
const type = (req.type || '').toLowerCase();
|
const type = (req.type || "").toLowerCase();
|
||||||
const status = (req.status || '').toLowerCase();
|
const status = (req.status || "").toLowerCase();
|
||||||
|
|
||||||
// Иконка типа
|
// Иконка типа
|
||||||
let typeIcon = 'bi-clipboard-check text-secondary';
|
let typeIcon = "bi-clipboard-check text-secondary";
|
||||||
if (type === 'vacation') typeIcon = 'bi-calendar-check text-primary';
|
if (type === "vacation") typeIcon = "bi-calendar-check text-primary";
|
||||||
else if (type === 'sick_leave') typeIcon = 'bi-capsule text-danger';
|
else if (type === "sick_leave") typeIcon = "bi-capsule text-danger";
|
||||||
else if (type === 'day_off') typeIcon = 'bi-umbrella-fill text-info';
|
else if (type === "day_off") typeIcon = "bi-umbrella-fill text-info";
|
||||||
else if (type === 'transfer') typeIcon = 'bi-box-arrow-right text-info';
|
else if (type === "transfer") typeIcon = "bi-box-arrow-right text-info";
|
||||||
else if (type === 'dismissal') typeIcon = 'bi-door-open text-warning';
|
else if (type === "dismissal") typeIcon = "bi-door-open text-warning";
|
||||||
|
|
||||||
// Текст типа
|
// Текст типа
|
||||||
const typeText = {
|
const typeText =
|
||||||
'vacation': 'Отпуск',
|
{
|
||||||
'sick_leave': 'Больничный',
|
vacation: "Отпуск",
|
||||||
'day_off': 'Отгул',
|
sick_leave: "Больничный",
|
||||||
'transfer': 'Перевод',
|
day_off: "Отгул",
|
||||||
'dismissal': 'Увольнение',
|
transfer: "Перевод",
|
||||||
'other': 'Другое'
|
dismissal: "Увольнение",
|
||||||
}[type] || req.type;
|
other: "Другое",
|
||||||
|
}[type] || req.type;
|
||||||
|
|
||||||
// Класс и текст статуса
|
// Класс и текст статуса
|
||||||
let statusBadgeClass = 'text-bg-light';
|
let statusBadgeClass = "text-bg-light";
|
||||||
let statusText = req.status;
|
let statusText = req.status;
|
||||||
if (status === 'draft') { statusBadgeClass = 'text-bg-info'; statusText = 'Черновик'; }
|
if (status === "draft") {
|
||||||
else if (status === 'pending') { statusBadgeClass = 'text-bg-warning'; statusText = 'На рассмотр.'; }
|
statusBadgeClass = "text-bg-info";
|
||||||
else if (status === 'approved') { statusBadgeClass = 'text-bg-success'; statusText = 'Одобрено'; }
|
statusText = "Черновик";
|
||||||
else if (status === 'rejected') { statusBadgeClass = 'text-bg-danger'; statusText = 'Отклонено'; }
|
} else if (status === "pending") {
|
||||||
else if (status === 'cancelled') { statusBadgeClass = 'text-bg-secondary'; statusText = 'Отменено'; }
|
statusBadgeClass = "text-bg-warning";
|
||||||
|
statusText = "На рассмотр.";
|
||||||
|
} else if (status === "approved") {
|
||||||
|
statusBadgeClass = "text-bg-success";
|
||||||
|
statusText = "Одобрено";
|
||||||
|
} else if (status === "rejected") {
|
||||||
|
statusBadgeClass = "text-bg-danger";
|
||||||
|
statusText = "Отклонено";
|
||||||
|
} else if (status === "cancelled") {
|
||||||
|
statusBadgeClass = "text-bg-secondary";
|
||||||
|
statusText = "Отменено";
|
||||||
|
}
|
||||||
|
|
||||||
// URL для сотрудника
|
// URL для сотрудника
|
||||||
const employeeUrl = `/employees/${req.employee?.id}/`;
|
const employeeUrl = `/employees/${req.employee?.id}/`;
|
||||||
const employeeName = req.employee?.id === userId ? 'Вы' : (req.employee?.full_name || req.employee?.display_name || 'Сотрудник');
|
const employeeName =
|
||||||
|
req.employee?.id === userId
|
||||||
|
? "Вы"
|
||||||
|
: req.employee?.full_name || req.employee?.display_name || "Сотрудник";
|
||||||
|
|
||||||
// Кнопки действий
|
// Кнопки действий
|
||||||
let actionButtons = '';
|
let actionButtons = "";
|
||||||
if (req.employee?.id !== userId && canProcess) {
|
if (req.employee?.id !== userId && canProcess) {
|
||||||
actionButtons = `
|
actionButtons = `
|
||||||
<button type="button" class="btn btn-ghost btn-sm text-success"
|
<button type="button" class="btn btn-ghost btn-sm text-success"
|
||||||
@ -387,11 +419,11 @@ export function initRequestListHandler(options) {
|
|||||||
<button type="button" class="btn btn-ghost btn-sm text-primary"
|
<button type="button" class="btn btn-ghost btn-sm text-primary"
|
||||||
data-bs-toggle="modal" data-bs-target="#reqEditModal"
|
data-bs-toggle="modal" data-bs-target="#reqEditModal"
|
||||||
data-id="${req.id}"
|
data-id="${req.id}"
|
||||||
data-title="${escapeHtml(req.title || '')}"
|
data-title="${escapeHtml(req.title || "")}"
|
||||||
data-type="${req.type || ''}"
|
data-type="${req.type || ""}"
|
||||||
data-date_from="${req.date_from || ''}"
|
data-date_from="${req.date_from || ""}"
|
||||||
data-date_to="${req.date_to || ''}"
|
data-date_to="${req.date_to || ""}"
|
||||||
data-comment="${escapeHtml(req.comment || '')}"
|
data-comment="${escapeHtml(req.comment || "")}"
|
||||||
data-status="${status}" title="Изменить">
|
data-status="${status}" title="Изменить">
|
||||||
<i class="bi-pencil"></i>
|
<i class="bi-pencil"></i>
|
||||||
</button>
|
</button>
|
||||||
@ -416,40 +448,66 @@ export function initRequestListHandler(options) {
|
|||||||
<!-- Заголовок и тип -->
|
<!-- Заголовок и тип -->
|
||||||
<div class="req-cell req-cell-title">
|
<div class="req-cell req-cell-title">
|
||||||
<div class="req-cell-content">
|
<div class="req-cell-content">
|
||||||
<strong>${escapeHtml(req.title || 'Без заголовка')}</strong>
|
<strong>${escapeHtml(req.title || "Без заголовка")}</strong>
|
||||||
${type ? `<span class="badge text-bg-light ms-1">${typeText}</span>` : ''}
|
${
|
||||||
${req.comment ? `<div class="small text-body-secondary mt-1">${escapeHtml(req.comment.substring(0, 100))}</div>` : ''}
|
type
|
||||||
|
? `<span class="badge text-bg-light ms-1">${typeText}</span>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
${
|
||||||
|
req.comment
|
||||||
|
? `<div class="small text-body-secondary mt-1">${escapeHtml(
|
||||||
|
req.comment.substring(0, 100)
|
||||||
|
)}</div>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Автор -->
|
<!-- Автор -->
|
||||||
<div class="req-cell req-cell-author">
|
<div class="req-cell req-cell-author">
|
||||||
<div class="req-cell-content">
|
<div class="req-cell-content">
|
||||||
${req.employee ? `<a href="${employeeUrl}" class="text-decoration-none">${employeeName}</a>` : ''}
|
${
|
||||||
|
req.employee
|
||||||
|
? `<a href="${employeeUrl}" class="text-decoration-none">${employeeName}</a>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Период -->
|
<!-- Период -->
|
||||||
<div class="req-cell req-cell-dates">
|
<div class="req-cell req-cell-dates">
|
||||||
<div class="req-cell-content text-mono">
|
<div class="req-cell-content text-mono">
|
||||||
${req.date_from || req.date_to ? `
|
${
|
||||||
${req.date_from ? formatDate(req.date_from) : '—'}<br>
|
req.date_from || req.date_to
|
||||||
${req.date_to ? formatDate(req.date_to) : '—'}
|
? `
|
||||||
` : '<span class="text-body-secondary">—</span>'}
|
${req.date_from ? formatDate(req.date_from) : "—"}<br>
|
||||||
|
${req.date_to ? formatDate(req.date_to) : "—"}
|
||||||
|
`
|
||||||
|
: '<span class="text-body-secondary">—</span>'
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Статус -->
|
<!-- Статус -->
|
||||||
<div class="req-cell req-cell-status">
|
<div class="req-cell req-cell-status">
|
||||||
<div class="req-cell-content">
|
<div class="req-cell-content">
|
||||||
${req.status ? `<span class="badge ${statusBadgeClass}">${statusText}</span>` : ''}
|
${
|
||||||
|
req.status
|
||||||
|
? `<span class="badge ${statusBadgeClass}">${statusText}</span>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Дата создания -->
|
<!-- Дата создания -->
|
||||||
<div class="req-cell req-cell-created">
|
<div class="req-cell req-cell-created">
|
||||||
<div class="req-cell-content small text-body-secondary">
|
<div class="req-cell-content small text-body-secondary">
|
||||||
${req.created_at ? formatDate(req.created_at) : '<span class="text-body-secondary">—</span>'}
|
${
|
||||||
|
req.created_at
|
||||||
|
? formatDate(req.created_at)
|
||||||
|
: '<span class="text-body-secondary">—</span>'
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -470,17 +528,25 @@ export function initRequestListHandler(options) {
|
|||||||
aria-controls="rcoll-${req.id}"
|
aria-controls="rcoll-${req.id}"
|
||||||
aria-expanded="false">
|
aria-expanded="false">
|
||||||
<i class="bi-chat-dots"></i>
|
<i class="bi-chat-dots"></i>
|
||||||
<span class="txt-open" data-role="count">${req.comments_count || 0}</span>
|
<span class="txt-open" data-role="count">${
|
||||||
|
req.comments_count || 0
|
||||||
|
}</span>
|
||||||
<span class="txt-close">Скрыть</span>
|
<span class="txt-close">Скрыть</span>
|
||||||
</button>
|
</button>
|
||||||
${req.attachment_url ? `
|
${
|
||||||
|
req.attachment_url
|
||||||
|
? `
|
||||||
<a href="${req.attachment_url}" class="btn btn-ghost btn-sm d-inline-flex align-items-center gap-1" target="_blank">
|
<a href="${req.attachment_url}" class="btn btn-ghost btn-sm d-inline-flex align-items-center gap-1" target="_blank">
|
||||||
<i class="bi-paperclip"></i><span>Вложение</span>
|
<i class="bi-paperclip"></i><span>Вложение</span>
|
||||||
</a>
|
</a>
|
||||||
` : ''}
|
`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<div id="rcoll-${req.id}" class="collapse" data-request-id="${req.id}" data-comments-collapse>
|
<div id="rcoll-${req.id}" class="collapse" data-request-id="${
|
||||||
|
req.id
|
||||||
|
}" data-comments-collapse>
|
||||||
<div class="comments-block p-3">
|
<div class="comments-block p-3">
|
||||||
<div class="d-flex align-items-center gap-2 mb-3">
|
<div class="d-flex align-items-center gap-2 mb-3">
|
||||||
<strong class="small mb-0">Комментарии</strong>
|
<strong class="small mb-0">Комментарии</strong>
|
||||||
@ -516,13 +582,13 @@ export function initRequestListHandler(options) {
|
|||||||
* @returns {string} CSRF токен
|
* @returns {string} CSRF токен
|
||||||
*/
|
*/
|
||||||
function getCsrfToken() {
|
function getCsrfToken() {
|
||||||
const name = 'csrftoken';
|
const name = "csrftoken";
|
||||||
let cookieValue = '';
|
let cookieValue = "";
|
||||||
if (document.cookie && document.cookie !== '') {
|
if (document.cookie && document.cookie !== "") {
|
||||||
const cookies = document.cookie.split(';');
|
const cookies = document.cookie.split(";");
|
||||||
for (let i = 0; i < cookies.length; i++) {
|
for (let i = 0; i < cookies.length; i++) {
|
||||||
const cookie = cookies[i].trim();
|
const cookie = cookies[i].trim();
|
||||||
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
if (cookie.substring(0, name.length + 1) === name + "=") {
|
||||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -538,12 +604,12 @@ export function initRequestListHandler(options) {
|
|||||||
*/
|
*/
|
||||||
function getStatusClass(status) {
|
function getStatusClass(status) {
|
||||||
const classes = {
|
const classes = {
|
||||||
'pending': 'warning',
|
pending: "warning",
|
||||||
'approved': 'success',
|
approved: "success",
|
||||||
'rejected': 'danger',
|
rejected: "danger",
|
||||||
'cancelled': 'secondary'
|
cancelled: "secondary",
|
||||||
};
|
};
|
||||||
return classes[status] || 'secondary';
|
return classes[status] || "secondary";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -553,10 +619,10 @@ export function initRequestListHandler(options) {
|
|||||||
*/
|
*/
|
||||||
function getStatusText(status) {
|
function getStatusText(status) {
|
||||||
const texts = {
|
const texts = {
|
||||||
'pending': 'На рассмотрении',
|
pending: "На рассмотрении",
|
||||||
'approved': 'Одобрено',
|
approved: "Одобрено",
|
||||||
'rejected': 'Отклонено',
|
rejected: "Отклонено",
|
||||||
'cancelled': 'Отменено'
|
cancelled: "Отменено",
|
||||||
};
|
};
|
||||||
return texts[status] || status;
|
return texts[status] || status;
|
||||||
}
|
}
|
||||||
@ -567,12 +633,12 @@ export function initRequestListHandler(options) {
|
|||||||
* @returns {string} Отформатированная дата
|
* @returns {string} Отформатированная дата
|
||||||
*/
|
*/
|
||||||
function formatDate(dateStr) {
|
function formatDate(dateStr) {
|
||||||
if (!dateStr) return '';
|
if (!dateStr) return "";
|
||||||
const date = new Date(dateStr);
|
const date = new Date(dateStr);
|
||||||
return date.toLocaleDateString('ru-RU', {
|
return date.toLocaleDateString("ru-RU", {
|
||||||
day: '2-digit',
|
day: "2-digit",
|
||||||
month: '2-digit',
|
month: "2-digit",
|
||||||
year: 'numeric'
|
year: "numeric",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -582,8 +648,8 @@ export function initRequestListHandler(options) {
|
|||||||
* @returns {string} Экранированная строка
|
* @returns {string} Экранированная строка
|
||||||
*/
|
*/
|
||||||
function escapeHtml(str) {
|
function escapeHtml(str) {
|
||||||
if (!str) return '';
|
if (!str) return "";
|
||||||
const div = document.createElement('div');
|
const div = document.createElement("div");
|
||||||
div.textContent = str;
|
div.textContent = str;
|
||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
@ -636,13 +702,13 @@ export function initRequestListHandler(options) {
|
|||||||
if (!searchInput) return;
|
if (!searchInput) return;
|
||||||
|
|
||||||
let searchTimeout;
|
let searchTimeout;
|
||||||
searchInput.addEventListener('input', (e) => {
|
searchInput.addEventListener("input", (e) => {
|
||||||
clearTimeout(searchTimeout);
|
clearTimeout(searchTimeout);
|
||||||
searchQuery = e.target.value;
|
searchQuery = e.target.value;
|
||||||
|
|
||||||
// Debounce: ждем 300ms после последнего ввода
|
// Debounce: ждем 300ms после последнего ввода
|
||||||
searchTimeout = setTimeout(() => {
|
searchTimeout = setTimeout(() => {
|
||||||
console.log('Search query:', searchQuery);
|
console.log("Search query:", searchQuery);
|
||||||
handleSearch();
|
handleSearch();
|
||||||
}, 300);
|
}, 300);
|
||||||
});
|
});
|
||||||
@ -672,11 +738,11 @@ export function initRequestListHandler(options) {
|
|||||||
setView,
|
setView,
|
||||||
setType,
|
setType,
|
||||||
setStatus,
|
setStatus,
|
||||||
destroy
|
destroy,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Экспорт для совместимости
|
// Экспорт для совместимости
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== "undefined") {
|
||||||
window.initRequestListHandler = initRequestListHandler;
|
window.initRequestListHandler = initRequestListHandler;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user