feat: dodaj bilješke widget i admin kalendar/notifikacije
Some checks failed
ERP CI/CD Pipeline / test (push) Has been cancelled
ERP CI/CD Pipeline / Deploy (server git pull + compose) (push) Has been cancelled

Uveden je novi Notes drawer s kalendar integracijom, zajednički DRY
left-slide mehanizam te međusobno isključivo prikazivanje s Kalendar
widgetom.

Backend i frontend su prošireni za servisne bilješke (uključujući slanje
na više servisera), dodan je users/servicers endpoint, te su uvedene
warning notifikacije kada serviser ima više od 2 zadatka isti dan.

U admin su dodane stavke za Bilješke i zaseban Kalendar zadataka (proxy
TaskCalendar) radi lakšeg operativnog pregleda.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
mariomitte
2026-07-20 06:33:36 +02:00
parent 61996bd1a0
commit 1a18498b30
20 changed files with 1312 additions and 113 deletions

View File

@@ -2,9 +2,11 @@
from io import BytesIO
import base64
from collections import OrderedDict
from datetime import timedelta
from pathlib import Path
from decimal import Decimal, InvalidOperation
from django.contrib.auth import get_user_model
from PIL import Image, UnidentifiedImageError
from django.conf import settings
@@ -14,6 +16,7 @@ from django.urls import reverse
from django.utils import timezone
from django.utils.html import escape
from django.db.models import Q
from django.db import transaction
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.utils import ImageReader
@@ -21,7 +24,7 @@ from reportlab.lib.styles import getSampleStyleSheet
from reportlab.pdfgen import canvas
from reportlab.platypus import Table, TableStyle, Paragraph
from .pdf_layout import register_unicode_fonts, draw_standard_header_footer
from rest_framework import viewsets, permissions, status
from rest_framework import viewsets, permissions, status, mixins
from rest_framework.decorators import action, api_view, permission_classes
from rest_framework.response import Response
from rest_framework.exceptions import PermissionDenied, ValidationError as DRFValidationError
@@ -32,6 +35,7 @@ from .models import (
Crane,
Vehicle,
VehicleNotification,
ServiceContextNote,
WorkOrder,
WorkOrderPhoto,
GeneratedWorkOrderPdf,
@@ -50,6 +54,8 @@ from .serializers import (
WorkOrderPhotoSerializer,
VehicleServiceRecordSerializer,
VehicleNotificationSerializer,
ServiceContextNoteSerializer,
ServiceContextNoteCreateSerializer,
VehicleServicePhotoSerializer,
VehicleServiceAttachmentSerializer,
)
@@ -67,6 +73,7 @@ from .tasks import (
)
register_unicode_fonts()
User = get_user_model()
def _fleet_assets_queryset_for_user(user, model, *, asset_type=None):
queryset = model.objects.select_related('client', 'assigned_servicer').filter(is_active=True)
@@ -99,6 +106,23 @@ def _work_orders_queryset_for_user(user):
return queryset.filter(creator=user)
def _is_supervisor_user(user):
return bool(getattr(user, 'is_staff', False) or (getattr(user, 'is_team_member', False) and not getattr(user, 'is_serviser', False)))
def _active_team_members_queryset():
return User.objects.filter(is_active=True, is_team_member=True).order_by('first_name', 'last_name', 'email')
def _service_context_notes_queryset_for_user(user):
return (
ServiceContextNote.objects
.select_related('created_by', 'recipient', 'work_order', 'task')
.filter(is_active=True, recipient=user, is_closed=False)
.order_by('note_date', '-created_at')
)
def _work_order_related_tasks_queryset(work_order):
from modules.task_management.models import Task
@@ -1994,6 +2018,110 @@ class WorkOrderInvoiceViewSet(viewsets.ModelViewSet):
instance.is_active = False
instance.save(update_fields=['is_active'])
class ServiceContextNoteViewSet(
mixins.ListModelMixin,
mixins.CreateModelMixin,
viewsets.GenericViewSet,
):
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return _service_context_notes_queryset_for_user(self.request.user)
def get_serializer_class(self):
if self.action == 'create':
return ServiceContextNoteCreateSerializer
return ServiceContextNoteSerializer
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data, context={'request': request})
serializer.is_valid(raise_exception=True)
payload = serializer.validated_data
user = request.user
audience = payload.get('audience', 'self')
target_users = payload.get('target_users') or []
note_text = payload.get('note')
note_date = payload.get('note_date')
work_order = payload.get('work_order')
task = payload.get('task')
is_supervisor = _is_supervisor_user(user)
can_manage_recipients = bool(is_supervisor or getattr(user, 'is_serviser', False))
if work_order and not _work_orders_queryset_for_user(user).filter(pk=work_order.pk).exists():
raise DRFValidationError({'work_order': 'Nemate dozvolu za odabrani putni nalog.'})
if task and not is_supervisor and str(getattr(task, 'assigned_to_id', '') or '') != str(user.id):
raise DRFValidationError({'task': 'Možete povezati samo svoj aktivni radni zadatak.'})
recipients = []
if audience == 'self':
recipients = [user]
elif audience == 'member':
recipients = list(target_users)
else:
if not can_manage_recipients:
raise DRFValidationError({'audience': 'Nemate dozvolu za slanje bilješke cijelom timu.'})
recipients = list(_active_team_members_queryset())
if not recipients:
raise DRFValidationError({'audience': 'Nema aktivnih članova tima za slanje bilješke.'})
created_notes = []
seen = OrderedDict()
for recipient in recipients:
seen[str(recipient.id)] = recipient
with transaction.atomic():
for recipient in seen.values():
note = ServiceContextNote.objects.create(
created_by=user,
recipient=recipient,
audience_source=audience,
work_order=work_order,
task=task,
note=note_text,
note_date=note_date,
)
created_notes.append(note)
NotificationService.create_notification(
recipient=recipient,
title='Nova bilješka servisnog konteksta',
message=note_text[:255],
level='info',
metadata={
'entity_type': 'service_note',
'service_note_id': str(note.id),
'work_order_id': str(work_order.id) if work_order else None,
'task_id': str(task.id) if task else None,
'section': 'dashboard',
'note_date': note_date.isoformat() if note_date else None,
},
send_email=False,
)
response_serializer = ServiceContextNoteSerializer(
_service_context_notes_queryset_for_user(user)[:20],
many=True,
)
return Response(
{
'created_count': len(created_notes),
'notes': response_serializer.data,
},
status=status.HTTP_201_CREATED,
)
@action(detail=True, methods=['post'], url_path='close')
def close(self, request, pk=None):
note = self.get_object()
if note.is_closed:
serializer = ServiceContextNoteSerializer(note)
return Response(serializer.data, status=status.HTTP_200_OK)
note.is_closed = True
note.closed_at = timezone.now()
note.save(update_fields=['is_closed', 'closed_at', 'updated_at'])
serializer = ServiceContextNoteSerializer(note)
return Response(serializer.data, status=status.HTTP_200_OK)
class VehicleNotificationViewSet(viewsets.ReadOnlyModelViewSet):
"""
List all notifications for the authenticated user and provide an action